#include <FL/Fl.H>
#include <FL/Fl_Button.H>
#include <FL/Fl_File_Chooser.H>
#include <FL/Fl_Round_Button.H>

#include <unistd.h>
#include <memory.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <alsa/asoundlib.h>
#include <stdint.h>
#include <assert.h>

extern "C"
{
	#include "codec2.h"
	#include "modem.h"
	#include "config.h"
}

/*
  Codec2 Proof Of Concept receiver

  Copyright (C) 2011,2012 Peter Lawrence

  All rights reserved.

  This program is free software; you can redistribute it and/or modify
  it under the terms of the GNU Lesser General Public License version 2.1, as
  published by the Free Software Foundation.  This program is
  distributed in the hope that it will be useful, but WITHOUT ANY
  WARRANTY; without even the implied warranty of MERCHANTABILITY or
  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public
  License for more details.

  You should have received a copy of the GNU Lesser General Public License
  along with this program; if not, see <http://www.gnu.org/licenses/>.
*/

#define WINDOW_X 240
#define WINDOW_Y 176
#define BORDER 8
#define MAX_DEMOD_SAMPLES 1216

//#define LOG_AUDIO

/* struct of the user's creation that newqpsk passes a pointer back through audioread() */

struct myaudio_struct
{
	/* ALSA handle */
	snd_pcm_t *handle;
	/* period size */
	snd_pcm_uframes_t frames, buffer_frames;
	/* channel count */
	unsigned int channels;
	/* soundcard buffers */
	short *buffer;
	/* read and write pointers */
	snd_pcm_uframes_t read_index, write_index;
};

struct modemchannel
{
	/* struct used by the modem library */
	struct demodulator *demod;
	/* handles for the soundcard(s) */
	struct myaudio_struct in, out;
	/* audio position, as used by audioread() and audiocurtime() */
	u_int16_t pos;
	/* carrier detect */
	int dcd;
};

/* raw_bps, obsolete, obsolete, tune_len, sync_len */
static const char *params[5] = { "2500", NULL, NULL, "0", "0" };

/* local static function prototypes */
static void idle_cb(void *arg);

/* static local variables */
static Fl_Window *win;
static Fl_Choice *choice_in, *choice_out;
static Fl_Button *start, *stop;
static int active;
static struct CODEC2 *codec2_ctx;
static unsigned char *c2_frame;
static uint32_t audio_buffer_size;
static short *c2_speech;
static void *modem_ctx;
#ifdef LOG_AUDIO
	static FILE *fd;
#endif

/*
TODO: override handler for Fl_Choice *choice to allow list of soundcards to updated each time the drop box is opened
this would allow someone to insert a USB soundcard into the PC and access it without having to re-run the program
*/

static int config_soundcard(struct myaudio_struct *pnt)
{
	int outcome = 0;
	snd_pcm_hw_params_t *params;
	unsigned int val;
	int dir;

	/* allocate params instance */
	snd_pcm_hw_params_alloca(&params);

	/* set to default values */
	snd_pcm_hw_params_any(pnt->handle, params);

	snd_pcm_hw_params_set_access(pnt->handle, params, SND_PCM_ACCESS_RW_INTERLEAVED);

	snd_pcm_hw_params_set_format(pnt->handle, params, SND_PCM_FORMAT_S16_LE);

	snd_pcm_hw_params_set_channels(pnt->handle, params, 1);

	val = OFDM_SAMPLE_RATE;
	snd_pcm_hw_params_set_rate_near(pnt->handle, params, &val, &dir);

	pnt->read_index = pnt->write_index = 0;

	if (0 == snd_pcm_hw_params(pnt->handle, params))
	{
		snd_pcm_hw_params_get_rate(params, &val, &dir);

		if (OFDM_SAMPLE_RATE == val)
		{
			snd_pcm_hw_params_get_period_size(params, &(pnt->frames), &dir);

			pnt->buffer_frames = pnt->frames;
			while ((2 * MAX_DEMOD_SAMPLES + pnt->frames) > pnt->buffer_frames)
				pnt->buffer_frames += pnt->frames;

			snd_pcm_hw_params_get_channels(params, &(pnt->channels));

			pnt->buffer = (short *)malloc(pnt->buffer_frames * sizeof(short) * pnt->channels);

			outcome = (pnt->buffer) ? 1 : 0;
		}
	}

	return outcome;
}

static void get_devname(Fl_Choice *c, char *devname, size_t count)
{
	char *pnt;

	strncpy(devname, c->text(), count);

	pnt = strchr(devname, '[');
	if ( pnt && (pnt != devname) )
		*(--pnt) = '\0';
}

static void start_cb(Fl_Widget* o, void *arg)
{
	struct modemchannel *chan = (struct modemchannel *)arg;
	char devname[64];
	int rc;

	get_devname(choice_in, devname, sizeof(devname));

	rc = snd_pcm_open(&(chan->in.handle), devname, SND_PCM_STREAM_CAPTURE, 0);

	if (0 == rc)
	{
		active = config_soundcard(&(chan->in));
	}

	if (active)
	{
		active = 0;

		get_devname(choice_out, devname, sizeof(devname));

		rc = snd_pcm_open(&(chan->out.handle), devname, SND_PCM_STREAM_PLAYBACK, 0);

		if (0 == rc)
		{
			active = config_soundcard(&(chan->out));
		}
		else
		{
			snd_pcm_drop(chan->in.handle);
			snd_pcm_close(chan->in.handle);
		}
	}

	if (!active)
	{
		fl_alert("soundcard would not accept configuration; start aborted\nALSA doesn't like quick configuration changes;\nit is worth trying again in a few seconds");
	}
	else
	{
#ifdef LOG_AUDIO
		fd = fopen("rec.raw", "wb");
#endif

		Fl::add_idle(idle_cb, chan);

		start->deactivate();
		stop->activate();
		choice_in->deactivate();
		choice_out->deactivate();
	}
}

static void stop_cb(Fl_Widget* o, void *arg)
{
	active = 0;
}

static void populate_choice(Fl_Choice *c)
{
	snd_ctl_t *cardHandle;
	char caption[64];
	int rc, cardNum, devNum;
	char *pnt;
	snd_pcm_info_t *pcmInfo;
	int subDevCount, i;

	c->clear();

//	c->add("default", 0, NULL, 0, 0);

	cardNum = -1;

	for (;;)
	{
		rc = snd_card_next(&cardNum);

		if ( (rc < 0) || (cardNum < 0) )
			break;

		if (0 == snd_card_get_name(cardNum, &pnt))
		{
			snprintf(caption, sizeof(caption), "hw:%i", cardNum);

			if (0 == snd_ctl_open(&cardHandle, caption, 0))
			{
				devNum = -1;

				for (;;)
				{
					rc == snd_ctl_pcm_next_device(cardHandle, &devNum);

					if ( (rc < 0) || (devNum < 0) )
						break;
					
					snd_pcm_info_alloca(&pcmInfo);
					memset(pcmInfo, 0, snd_pcm_info_sizeof());

					snd_pcm_info_set_device(pcmInfo, devNum);

					snd_pcm_info_set_stream(pcmInfo, (c == choice_out) ? SND_PCM_STREAM_PLAYBACK : SND_PCM_STREAM_CAPTURE);

					subDevCount = snd_pcm_info_get_subdevices_count(pcmInfo);

					i = 0;
					do
					{
						if (subDevCount > 1)
							snprintf(caption, sizeof(caption), "hw:%i,%i,%i [%s]\n", cardNum, devNum, i, pnt);
						else
							snprintf(caption, sizeof(caption), "hw:%i,%i [%s]\n", cardNum, devNum, pnt);

						c->add(caption, 0, NULL, 0, 0);
					} while (++i < subDevCount);
				}
			}
		}
	}
}

int main (int argc, char *argv[])
{
	int vpos, i;
	unsigned int sample_rate, bps;
	struct modemchannel chan;

	memset(&chan, 0, sizeof(chan));

	/* initialize instances of Codec2 (one for each bitrate) */
	codec2_ctx = codec2_create(CODEC2_MODE_2500);

	if (NULL == codec2_ctx)
	{
		fl_alert("Codec2 did not initialize; software terminating");
		return -1;
	}

	/* determine worst case Codec2 frame size */
	i = codec2_bits_per_frame(codec2_ctx);

	/* putwords() is currently hard-coded for 50 bits */
	assert(50 == i);

	/* allocate memory for Codec2 frame size */
	c2_frame = (uint8_t *)malloc((i + 7) / 8);

	/* determine worst case Codec2 samples per frame */
	audio_buffer_size = codec2_samples_per_frame(codec2_ctx);

	/* scale value from units of samples to bytes */
	audio_buffer_size *= sizeof(short);

	c2_speech = (short *)malloc(audio_buffer_size);

	if (!c2_frame || !c2_speech)
	{
		fl_alert("buffer allocation failed; software terminating");
		return -1;
	}

	/* select the demodulator of choice (this is a legacy of the soundmodem library) */
	chan.demod = &newqpskdemodulator;

	/* config (and be informed of the sample rate) */
	modem_ctx = chan.demod->config(&chan, &sample_rate, params);

	/*
	ignore what the library wants; this is the sample rate we'll use
	*/
	sample_rate = OFDM_SAMPLE_RATE;

	/* init comes after config (and tells us the data rate) */
	chan.demod->init(modem_ctx, sample_rate, &bps);

	/*
	start constructing the window
	*/

	win = new Fl_Window(WINDOW_X, WINDOW_Y, "Codec2 RCVR v0.3");

	win->begin();

	vpos = BORDER;

	start = new Fl_Button(BORDER, vpos, (WINDOW_X - 3 * BORDER) / 2, 30, "St&art");
	start->activate();
	start->callback(start_cb, &chan);
	stop = new Fl_Button(WINDOW_X - BORDER - (WINDOW_X - 3 * BORDER) / 2, vpos, (WINDOW_X - 3 * BORDER) / 2, 30, "S&top");
	stop->deactivate();
	stop->callback(stop_cb, &chan);
	vpos += 40;

	new Fl_Box(BORDER, vpos, WINDOW_X - 2 * BORDER, 16, "receiver input:");
	vpos += 20;

	choice_in = new Fl_Choice(BORDER, vpos, WINDOW_X - 2 * BORDER, 30);
	populate_choice(choice_in);
	if (choice_in->size())
	{
		choice_in->value(0);
	}
	else
	{
		fl_alert("no soundcard found; software terminating");
		return -1;
	}
	vpos += 40;

	new Fl_Box(BORDER, vpos, WINDOW_X - 2 * BORDER, 16, "decoded speech output:");
	vpos += 20;

	choice_out = new Fl_Choice(BORDER, vpos, WINDOW_X - 2 * BORDER, 30);
	populate_choice(choice_out);
	choice_out->value(0); /* omit check for zero entries, as previous check on choice_in *should* be sufficient */
	vpos += 40;

	win->end();

	win->show();

	/*
	finished constructing the window
	*/

	Fl::lock();

	/* hand the keys to FLTK; it does all the work now */
	i = Fl::run();

	/* free up memory allocated */
	codec2_destroy(codec2_ctx);
	free(c2_frame);
	free(c2_speech);

	return i;
}

static void attempt_audio_write(struct modemchannel *chan, int final)
{
	int rc;

	if ( ( chan->out.write_index >= (chan->out.frames * chan->out.channels) ) || ( final && chan->out.write_index ) )
	{
		do
		{
			rc = snd_pcm_writei(chan->out.handle, chan->out.buffer, chan->out.frames);

			if (rc < 0)
			{
				snd_pcm_recover(chan->out.handle, rc, 1);
			}
		} while (rc < 0);

		memmove(chan->out.buffer, &(chan->out.buffer[chan->out.frames * chan->out.channels]), chan->out.frames * sizeof(short) * chan->out.channels);
		if (!final)
			chan->out.write_index -= chan->out.frames * chan->out.channels;
		else
			chan->out.write_index = 0;
		memset(&(chan->out.buffer[chan->out.write_index]), 0, chan->out.buffer_frames * sizeof(short) * chan->out.channels - chan->out.write_index);
	}
}

static void idle_cb(void *arg)
{
	static uint32_t i;
	struct modemchannel *chan = (struct modemchannel *)arg;
	int rc;

	if (active)
	{
		rc = snd_pcm_readi(chan->in.handle, (void *)&(chan->in.buffer[chan->in.write_index]), chan->in.frames);

		if (rc > 0)
		{
			chan->in.write_index += (chan->in.channels * rc);


			while ( (chan->in.read_index + (chan->in.channels * MAX_DEMOD_SAMPLES)) < chan->in.write_index )
			{
				/* perform some demodulation */
				chan->demod->demodulate(modem_ctx);
			}

			memmove(chan->in.buffer, &(chan->in.buffer[chan->in.read_index]), (chan->in.write_index - chan->in.read_index) * sizeof(short) * chan->in.channels);
			chan->in.write_index -= chan->in.read_index;
			chan->in.read_index = 0;

			attempt_audio_write(chan, !chan->dcd);

			return;
		}
	}

#ifdef LOG_AUDIO
	fclose(fd);
#endif

	snd_pcm_drop(chan->in.handle);
	snd_pcm_close(chan->in.handle);
	snd_pcm_drop(chan->out.handle);
	snd_pcm_close(chan->out.handle);

//	free(chan->in.buffer);
//	free(chan->out.buffer);

	start->activate();
	stop->deactivate();
	choice_in->activate();
	choice_out->activate();

	win->color( FL_BACKGROUND_COLOR );
	win->redraw();

	Fl::remove_idle(idle_cb, arg);
}

void audioread(struct modemchannel *chan, int16_t *samples, unsigned int nr, u_int16_t tim)
{
	unsigned int i;

	for (i = 0; i < nr; i++)
	{
		samples[i] = chan->in.buffer[chan->in.read_index];
#ifdef LOG_AUDIO
		fwrite(&(samples[i]), sizeof(short), 1, fd);
#endif
		chan->in.read_index += chan->in.channels;
	}

	chan->pos = tim;
}

void putwords(struct modemchannel *chan, const unsigned *words, unsigned int len)
{
	unsigned int i, j;

	assert(2 == len);

	/* recover the 50 bits of Codec2 data */
	c2_frame[0] = (u_int8_t)((words[0] & 0x01FE0000) >> 17);
	c2_frame[1] = (u_int8_t)((words[0] & 0x0001FE00) >> 9);
	c2_frame[2] = (u_int8_t)((words[0] & 0x000001FE) >> 1);
	c2_frame[3] = (u_int8_t)((words[1] & 0x01FE0000) >> 17);
	c2_frame[4] = (u_int8_t)((words[1] & 0x0001FE00) >> 9);
	c2_frame[5] = (u_int8_t)((words[1] & 0x000001FE) >> 1);
	c2_frame[6] = (words[0] & 1) ? 0x80 : 0x00;
	c2_frame[6] |= (words[1] & 1) ? 0x40 : 0x00;

	/* decode the Codec2 data into speech*/
	codec2_decode(codec2_ctx, c2_speech, c2_frame);

	/* write the speech out to the designated soundcard */
	for (i = 0; i < (audio_buffer_size / sizeof(short)); i++)
		for (j = 0; j < (OFDM_SAMPLE_RATE * chan->out.channels / 8000); j++)
			chan->out.buffer[chan->out.write_index++] = c2_speech[i];
}

/* library expects logprintf(); this function directs the output to stderr */

void logprintf(unsigned int level, const char *fmt, ...)
{
#if 0
	va_list arg;

	fprintf(stderr, "lev%u: ", level);
	va_start(arg, fmt);
	vfprintf(stderr, fmt, arg);
	va_end(arg);
#endif
}

/* the library provides a Data Carrier Detect signal */

void pktsetdcd(struct modemchannel *chan, int dcd)
{
	if (win)
	{
		/* make the window background green when receiving */
		win->color( (dcd) ? FL_GREEN : FL_BACKGROUND_COLOR );
		win->redraw();
	}

	chan->dcd = dcd;
}

/* the library ostensibly has provision to be aware of discontinuities in the audio stream */

u_int16_t audiocurtime(struct modemchannel *chan)
{
	return chan->pos;
}

